using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace Framework
{
    /// <summary>
    /// Provides various extensions that can be applied to POCO style objects
    /// </summary>
    public class PocoExtension
    {
        private static readonly object syncLock = new object();

        /// <summary>
        /// Dictionary (Cache) to hold type initialization methods for setting default values on a POCO object
        /// </summary>
        static private readonly Dictionary<Type, Action<Object>> DefaultValueTypeInitializers = new Dictionary<Type, Action<Object>>();

        /// <summary>
        /// Dictionary (Cache) to hold type initialization methods for limiting string length values on a POCO object
        /// </summary>
        static private readonly Dictionary<Type, List<StringLengthLimitProperty>> StringLengthLimitTypeInitializers = new Dictionary<Type, List<StringLengthLimitProperty>>();

        #region ApplyDefaultValues
        /// <summary>
        /// Data Structure for selected properties and there default values
        /// </summary>
        private class DefaultValueDto
        {
            public PropertyInfo Info { get; set; }

            public object DefaultValue { get; set; }
        }

        /// <summary>
        /// Implements precompiled setters with embedded constant values from DefaultValueAttributes
        /// </summary>
        /// <param name="obj">The object to apply default values to.</param>
        /// <remarks>
        /// http://www.codeproject.com/KB/dotnet/DefValInit.aspx
        /// </remarks>
        /// <example>
        /// public class AOPInitialized
        /// {
        ///     public AOPInitialized()
        ///     {
        ///         this.ApplyDefaultValues();
        ///     }
        ///     [DefaultValue(3)]
        ///     public int IntProperty { get; set; }
        ///     
        ///     [DefaultValue(3)]
        ///     public long LongProperty { get; set; }
        ///     
        ///     [DefaultValue(true)]
        ///     public bool BoolProptrty { get; set; }
        /// }
        /// </example>
        public void ApplyDefaultValues(object obj)
        {
            Action<object> setter;

            // Attempt to get it from cache
            if (!DefaultValueTypeInitializers.TryGetValue(obj.GetType(), out setter))
            {
                // If no initializers are added do nothing
                setter = o => { };

                // Iterate through each property
                var objectTypeParam = Expression.Parameter(typeof(object), "this");

                foreach (var prop in QueryDefaultValueProperties(obj))
                {
                    // Build the Lambda expression
                    // Create expression describing call to approptiate setter and passing it instance and value parameters
                    var defaultValue = GetDefaultValue(obj, prop);
                    var instance = Expression.TypeAs(objectTypeParam, obj.GetType());
                    var setExpression = Expression.Call(instance, prop.Info.GetSetMethod(), defaultValue);
                    var setLambda = Expression.Lambda<Action<Object>>(setExpression, objectTypeParam);

                    // Add this action to multicast delegate
                    setter += setLambda.Compile();
                }

                // Save in the type cache
                DefaultValueTypeInitializers.Add(obj.GetType(), setter);
            }

            // Initialize member properties
            setter(obj);
        }

        private static IEnumerable<DefaultValueDto> QueryDefaultValueProperties(object obj)
        {
            // Query for writable properites with a single DefaultValue Attribute
            return from prop in obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty)
                             where prop.GetCustomAttributes(typeof(DefaultValueAttribute), false).Length == 1
                             select new DefaultValueDto
                             {
                                 Info = prop,
                                 DefaultValue = ((DefaultValueAttribute)prop.GetCustomAttributes(typeof(DefaultValueAttribute), false)[0]).Value
                             };
        }

        private static Expression GetDefaultValue(object obj, DefaultValueDto prop)
        {
            // Make sure types do match
#if DEBUG
            try
            {
                // Create constant expression with value from DefaultValue attribute and convert it into appropriate type
                return Expression.Constant(prop.DefaultValue, prop.Info.PropertyType);
            }
            catch (InvalidOperationException e)
            {
                var error = "Type of DefaultValueAttribute({3}{0}{3}) does not match type of property {1}.{2}".FormatWith(
                                             prop.DefaultValue,
                                             obj.GetType().Name,
                                             prop.Info.Name,
                                             ((typeof(string) == prop.DefaultValue.GetType()) ? "\"" : ""));

                throw (new InvalidOperationException(error, e));
            }
#else
            return Expression.Constant(prop.DefaultValue, prop.Info.PropertyType);
#endif
        }
        #endregion

        #region ResetDefaultValues (Removed for now)
        /* This code seems wrong, it is using the same cache object as ApplyDefaultValues, if we need it then look into how it works
        /// <summary>
        /// Implements cache of ResetValue delegates
        /// </summary>
        public static void ResetDefaultValues(Object _this)
        {
            Action<Object> setter = null;

            // Attempt to get it from cache
            if (!DefaultValueTypeInitializers.TryGetValue(_this.GetType(), out setter))
            {
                // Init delegate with empty body,
                // If no initializers are added do nothing
                setter = (o) => { };

                // Go throu each property and compile Reset delegates
                foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(_this))
                {
                    // Add only these which values can be reset
                    if (prop.CanResetValue(_this))
                        setter += prop.ResetValue;
                }

                // Save in the type cache
                DefaultValueTypeInitializers.Add(_this.GetType(), setter);
            }

            // Initialize member properties
            setter(_this);
        }
        */
        #endregion

        #region ApplyStringLengthLimits
        /// <summary>
        /// Ensures that any string property that uses <typeparamref name="System.ComponentModel.DataAnnotations.StringLength"/> does not break that limit by trimming the strings in question.
        /// This can be applied to SQL entity type POCO's so that you don't get overflow errors if the data is to long for the field.
        /// </summary>
        /// <param name="obj">The object to apply default values to.</param>
        /// <remarks>
        /// Implements precompiled property trimming in compiled cache for performance reasons.
        /// </remarks>
        /// <example>
        /// public class MyEntity
        /// {
        ///     [System.ComponentModel.DataAnnotations.StringLength(20)]
        ///     public string StringLength20 { get; set; }
        ///     
        ///     [System.ComponentModel.DataAnnotations.StringLength(100)]
        ///     public string StringLength100 { get; set; }
        ///     
        ///     public string StringLengthAnyLength { get; set; }
        ///     
        ///     public string StringLengthNull { get; set; }
        /// }
        /// 
        /// var string1000 = StrUtil.Replicate(1000, "*");
        /// 
        /// var entity = new MyEntity
        /// {
        ///     StringLengthAnyLength = string1000,
        ///     StringLength100 = string1000,
        ///     StringLengthNull = null
        /// };
        /// 
        /// var pocoExtension = new PocoExtension();
        /// 
        /// pocoExtension.ApplyStringLengthLimits(entity);
        /// 
        /// Assert.AreEqual(entity.StringLength100.Length, 100);
        /// </example>
        public void ApplyStringLengthLimits(object obj)
        {
            lock (syncLock)
            {
                List<StringLengthLimitProperty> limitProperties;

                // Attempt to get it from cache
                if (!StringLengthLimitTypeInitializers.TryGetValue(obj.GetType(), out limitProperties))
                {
                    // If no initializers are added do nothing
                    limitProperties = QueryStringLengthLimitProperties(obj).Select(prop => new StringLengthLimitProperty(prop.Info, prop.MaximumLength)).ToList();

                    // Save in the type cache
                    StringLengthLimitTypeInitializers.Add(obj.GetType(), limitProperties);
                }
                limitProperties.ForEach(limit => limit.LimitLength(obj));
            }
        }

        private static IEnumerable<StringLengthLimitDto> QueryStringLengthLimitProperties(object obj)
        {
            // Query for writable properites with a single DefaultValue Attribute
            return from prop in obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty | BindingFlags.GetProperty)
                   where
                        prop.GetGetMethod().ReturnType == typeof(string) &&
                        prop.GetCustomAttributes(typeof(StringLengthAttribute), false).Length == 1
                   select new StringLengthLimitDto
                   {
                       Info = prop,
                       MaximumLength = ((StringLengthAttribute)prop.GetCustomAttributes(typeof(StringLengthAttribute), false)[0]).MaximumLength
                   };
        }

        private class StringLengthLimitDto
        {
            public PropertyInfo Info { get; set; }

            public int MaximumLength { get; set; }
        }

        private class StringLengthLimitProperty
        {
            public StringLengthLimitProperty(PropertyInfo property, int maximumLength)
            {
                Property = new FastProperty(property);

                MaximumLength = maximumLength;
            }

            private FastProperty Property { get; set; }

            private int MaximumLength { get; set; }

            public void LimitLength(object instance)
            {
                var currentValue = Property.Get(instance) as string;

                Property.Set(instance, StrUtil.Left(currentValue, MaximumLength));
            }
        }

        #endregion
    }
}